<head>
    <style>
        .pass, .fail {
            height: 30px;
            line-height: 30px;
            color: white;
            display: block;
            border: 1px solid black;
            margin: 3px;
            padding: 3px;
        }

        .pass { background-color: green; }
        .fail { background-color: red; }
    </style>
</head>
<body>
    <div></div>
    <div id=test1></div>

    <script>
        let test0 = document.getElementsByTagName('div')[0];
        let test1 = document.getElementById('test1');

        const bindMethodsForTest = (method) => {
            let failures = 0;

            const pass = (message) => {
                if (failures > 0)
                    return;

                const span = document.createElement('span');
                span.innerHTML = `Pass! ${method}`;
                span.setAttribute('class', 'pass');
                document.body.appendChild(span);
            };

            const fail = (message) => {
                const span = document.createElement('span');
                span.innerHTML = `Fail! ${method}: ${message}`;
                span.setAttribute('class', 'fail');
                document.body.appendChild(span);
                ++failures;
            };

            return [pass, fail];
        };

        (() => {
            const [pass, fail] = bindMethodsForTest('hasAttributes');

            if (test0.hasAttributes())
                fail('test0 should not have any attributes');
            if (!test1.hasAttributes())
                fail('test1 should have attributes');

            pass();
        })();

        (() => {
            const [pass, fail] = bindMethodsForTest('hasAttribute');

            if (test0.hasAttribute('id'))
                fail('test0 should not have an "id" attribute');
            if (test0.hasAttribute('foo'))
                fail('test0 should not have a "foo" attribute');

            if (!test1.hasAttribute('id'))
                fail('test1 should have an "id" attribute');
            if (test1.hasAttribute('foo'))
                fail('test1 should not have a "foo" attribute');

            pass();
        })();

        (() => {
            const [pass, fail] = bindMethodsForTest('getAttribute');

            if ((attr = test0.getAttribute('id')) !== null)
                fail(`test0 should not have an "id" attribute but has "${attr}"`);
            if ((attr = test0.getAttribute('foo')) !== null)
                fail(`test0 should not have a "foo" attribute but has "${attr}"`);

            if ((attr = test1.getAttribute('id')) !== 'test1')
                fail(`test1 should have an "id" attribute of "test1" but has "${attr}"`);
            if ((attr = test1.getAttribute('foo')) !== null)
                fail(`test1 should not have a "foo" attribute but has "${attr}"`);

            pass();
        })();

        (() => {
            const [pass, fail] = bindMethodsForTest('setAttribute');

            try {
                test0.setAttribute('', '');
                fail('Expected setting an empty attribute name to throw');
            } catch (error) {
                if (error.name !== 'InvalidCharacterError')
                    fail(`Expected exception to be a DOMException of type InvalidCharacterError but is "${error.name}"`);
            }

            test0.setAttribute('foo', '123');

            if (test0.attributes.length !== 1)
                fail('test0 should have 1 attribute');
            if ((attr = test0.getAttribute('foo')) !== '123')
                fail(`test0 should have a "foo" attribute of "123" but has "${attr}"`);

            test0.setAttribute('bar', '456');

            if (test0.attributes.length !== 2)
                fail(`test0 should have 2 attributes but has ${test0.attributes.length}`);
            if ((attr = test0.getAttribute('bar')) !== '456')
                fail(`test0 should have a "bar" attribute of "456" but has "${attr}"`);

            test0.setAttribute('foo', '789');

            if (test0.attributes.length !== 2)
                fail(`test0 should have 2 attributes but has ${test0.attributes.length}`);
            if ((attr = test0.getAttribute('foo')) !== '789')
                fail(`test0 should have a "foo" attribute of "789" but has "${attr}"`);

            try {
                const foo = test0.attributes.item(0);
                test0.attributes.setNamedItem(foo);
            } catch (error) {
                fail(`Re-assigning an attribute to the same element should not throw: ${error}`);
            }

            try {
                const foo = test0.attributes.item(0);
                test1.attributes.setNamedItem(foo);

                fail('Expected re-assigning an attribute to throw');
            } catch (error) {
                if (error.name !== 'InUseAttributeError')
                    fail(`Expected exception to be a DOMException of type InUseAttributeError but is "${error.name}"`);
            }

            pass();
        })();

        (() => {
            const [pass, fail] = bindMethodsForTest('removeAttribute');

            test0.setAttribute('foo', '123');

            if ((attr = test0.getAttribute('foo')) !== '123')
                fail(`test0 should have a "foo" attribute of "123" but has "${attr}"`);

            test0.removeAttribute('foo');

            if (test0.hasAttribute('foo'))
                fail('test0 should not have a "foo" attribute');

            try {
                test0.removeAttribute('foo');
            } catch (error) {
                fail(`Removing a non-existent attribute from an element should not throw: ${error}`);
            }

            try {
                test0.attributes.removeNamedItem('foo');
                fail('Expected removing a non-existent attribute from a named node map to throw');
            } catch (error) {
                if (error.name !== 'NotFoundError')
                    fail(`Expected exception to be a DOMException of type NotFoundError but is "${error.name}"`);
            }

            pass();
        })();

        (() => {
            const [pass, fail] = bindMethodsForTest('attributes.length');
            const attributes = test1.attributes;

            if (attributes.length !== 1)
                fail(`test1 should have 1 attribute but has ${attributes.length}`);

            test1.setAttribute('foo', '123');

            if (attributes.length !== 2)
                fail(`test1 should have 2 attributes but has ${attributes.length}`);

            test1.removeAttribute('foo');

            if (attributes.length !== 1)
                fail(`test1 should have 1 attribute but has ${attributes.length}`);

            pass();
        })();

        (() => {
            const [pass, fail] = bindMethodsForTest('attributes.items');

            const attribute0 = test1.attributes.item(0);

            if (attribute0.name !== 'id')
                fail(`test1's first attribute's name should be "id" but is "${attribute0.name}"`);
            if (attribute0.value !== 'test1')
                fail(`test1's first attribute's value should be "test1" but is "${attribute0.value}"`);

            const attribute1 = test1.attributes.item(1);
            if (attribute1 !== null)
                fail(`test1 should not have a second attribute but has ${attribute1}`);

            pass();
        })();
    </script>
</body>
